import { Fragment, FragmentsGroup, Serializer } from "bim-fragment";
import * as THREE from "three";

/**
 * Object that can efficiently load binary files that contain
 * [fragment geometry](https://github.com/ThatOpen/engine_fragment).
 */
export class FragmentManager {
  static readonly uuid = "fef46874-46a3-461b-8c44-2922ab77c806" as const;

  /** {@link Component.enabled} */
  enabled = true;

  /** All the created [fragments](https://github.com/ThatOpen/engine_fragment). */
  list: { [guid: string]: Fragment } = {};

  groups: FragmentsGroup[] = [];

  baseCoordinationModel = "";

  private _loader = new Serializer();

  /** The list of meshes of the created fragments. */
  get meshes() {
    const allMeshes: THREE.Mesh[] = [];
    for (const fragID in this.list) {
      allMeshes.push(this.list[fragID].mesh);
    }
    return allMeshes;
  }

  constructor() {}

  /** {@link Component.get} */
  get(): Fragment[] {
    return Object.values(this.list);
  }

  /** {@link Component.get} */
  async dispose() {
    for (const group of this.groups) {
      group.dispose(true);
    }
    this.groups = [];
    this.list = {};
  }

  async disposeGroup(group: FragmentsGroup) {
    const fragmentIDs: string[] = [];
    for (const fragment of group.items) {
      fragmentIDs.push(fragment.id);
      delete this.list[fragment.id];
    }
    group.dispose(true);
    const index = this.groups.indexOf(group);
    this.groups.splice(index, 1);
  }

  /** Disposes all existing fragments */
  reset() {
    for (const id in this.list) {
      const fragment = this.list[id];
      fragment.dispose();
    }
    this.list = {};
  }

  /**
   * Loads one or many fragments into the scene.
   * @param data - the bytes containing the data for the fragments to load.
   * @param coordinate - whether this fragmentsgroup should be federated with the others.
   * @returns the list of IDs of the loaded fragments.
   */
  async load(data: Uint8Array, coordinate = true) {
    const model = this._loader.import(data);
    const ids: string[] = [];
    for (const fragment of model.items) {
      fragment.group = model;
      this.list[fragment.id] = fragment;
      ids.push(fragment.id);
    }
    if (coordinate) {
      this.coordinate([model]);
    }
    this.groups.push(model);
    return model;
  }

  /**
   * Export the specified fragments.
   * @param group - the fragments group to be exported.
   * @returns the exported data as binary buffer.
   */
  export(group: FragmentsGroup) {
    return this._loader.export(group);
  }

  coordinate(models = this.groups) {
    const isFirstModel = this.baseCoordinationModel.length === 0;
    if (isFirstModel) {
      const first = models.pop();
      if (!first) {
        return;
      }
      this.baseCoordinationModel = first.uuid;
    }

    if (!models.length) {
      return;
    }

    const baseModel = this.groups.find(
      (group) => group.uuid === this.baseCoordinationModel
    );

    if (!baseModel) {
      console.log("No base model found for coordination!");
      return;
    }

    for (const model of models) {
      if (model === baseModel) {
        continue;
      }
      model.position.set(0, 0, 0);
      model.rotation.set(0, 0, 0);
      model.scale.set(1, 1, 1);
      model.updateMatrix();
      model.applyMatrix4(model.coordinationMatrix.clone().invert());
      model.applyMatrix4(baseModel.coordinationMatrix);
    }
  }
}
